package com.example.demo.util;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Component
public class RedisLockUtil {

    /**
     * key前缀
     */
    public static final String PREFIX = "Lock:";


    /**
     * 保存锁的value
     */
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();


    private static final Charset UTF8 = Charset.forName("UTF-8");
    /**
     * 释放锁脚本
     */
    private static final String UNLOCK_LUA;

    /*
     * 释放锁脚本，原子操作
     */
    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }


    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @param key
     * @param requireTimeOut 获取锁超时时间 单位：毫秒
     * @param lockTimeOut    过期时间 单位：毫秒
     * @return
     */
    public Boolean lock(String key, String uniqueId, long requireTimeOut, long lockTimeOut) {
        //可重入锁判断
        String wrapLockKey = wrapLockKey(key);
        long threadId = Thread.currentThread().getId();
        String filed = uniqueId + threadId;
        HashOperations hashOperations = redisTemplate.opsForHash();
        //判断有没有key存在
        Boolean hasKey = redisTemplate.hasKey(wrapLockKey);
        if (hasKey && isReentrantLock(hashOperations, filed, wrapLockKey)) {
            return true;
        }
        long currentTimeMillis = System.currentTimeMillis();
        long end = currentTimeMillis + requireTimeOut;
        try {
            while (currentTimeMillis < end) {
                if (setNX(hashOperations, wrapLockKey, filed, lockTimeOut)) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


    private boolean setNX(HashOperations hashOperations, String key, String filed, long expire) {
        //todo 不是原子性
        if (hashOperations.putIfAbsent(key, filed, 1)) {
            threadLocal.set(filed);
            return redisTemplate.expire(key, expire, TimeUnit.MILLISECONDS);
        }
        return false;

    }

    /**
     * 是否为重入锁
     */
    private boolean isReentrantLock(HashOperations hashOperations, String filed, String wrapLockKey) {
        Boolean hasFiled = hashOperations.hasKey(wrapLockKey, filed);
        if (hasFiled) {
            hashOperations.increment(wrapLockKey, filed, 1);
            return true;
        }
        return false;
    }


    public boolean release(String key) {
        if (StringUtils.isBlank(key)) {
            return false;
        }
        String wrapLockKey = wrapLockKey(key);
        Boolean hasKey = redisTemplate.hasKey(wrapLockKey);
        if (!hasKey) {
            return false;
        }
        List<String> keyList = new ArrayList<>();
        keyList.add(wrapLockKey);
        return (boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Boolean result = connection
                    .set(key.getBytes(UTF8),
                            threadLocal.get().getBytes(UTF8),
                            Expiration.milliseconds(2000),
                            RedisStringCommands.SetOption.SET_IF_ABSENT);
            return result;
        });
    }

    private String wrapLockKey(String key) {
        return PREFIX + key;
    }
}

